package it.tukano.swingdeco.table;

import it.tukano.collections.ArrayListExt;
import it.tukano.collections.LinkedListExt;
import it.tukano.collections.ListExt;
import it.tukano.fun.Function1;
import it.tukano.fun.Function2;
import it.tukano.fun.Function3;
import it.tukano.fun.Tuple2;
import it.tukano.log.Log;
import it.tukano.swingdeco.controls.JPopupMenuExt;
import it.tukano.swingdeco.event.MouseAdapterExt;
import it.tukano.swingdeco.event.MouseEventExt;
import it.tukano.xprint.XPrint;
import it.tukano.xprint.XPrint.XTable;
import it.tukano.xprint.XPrint.XTableCell;
import it.tukano.xprint.XPrint.XTableRow;
import java.awt.Component;
import java.awt.Point;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import javax.swing.text.JTextComponent;

/**
 * A JTable extension
 * @author pgi
 */
public class JTableExt extends JTable implements Iterable<ListExt<?>> {

    /* Serial version of this class */
    private static final long serialVersionUID = 0;

    private boolean[] editableColumns;
    private boolean hasRowNames;

    private JScrollPane scroller;
    private boolean notEditable;
    private final Map<Integer, Class<?>> colClasses = new HashMap<Integer, Class<?>>();
    private final Map<Integer, TableCellEditor> columnEditors = new HashMap<Integer, TableCellEditor>();
    private final Map<Integer, Object[]> rowTags = new HashMap<Integer, Object[]>();
    private JPopupMenuExt defaultTextEditorPopup;

    /**
     * Instance initializer
     */
    public JTableExt() {
    }
    
    public JTableExt(DefaultTableModel model) {
        super(model);
    }

    public JTableExt(int rowCount, int colCount) {
        super(rowCount, colCount);
    }

    public JTableExt(int rowCount, int colCount, String... columnNames) {
        super(rowCount, colCount);
        getModel().setColumnIdentifiers(columnNames);
    }
    
    /** Sets the editor used to edit the values in a given column */
    public JTableExt setEditorForColumn(TableCellEditor editor, int columnIndex) {
        columnEditors.put(columnIndex, editor);
        return this;
    }
    
    /** Executes a function for each row of this table. Returns when the function returns false*/
    public void forEachRow(Function1<Object[], Boolean> fun) {
        int rc = getRowCount();
        int cc = getColumnCount();
        Object[] rowCache = new Object[cc];
        for (int i = 0; i < rc; i++) {
            for (int j = 0; j < cc; j++) {
                rowCache[j] = getValueAt(i, j);
            }
            boolean goOn = fun.apply(rowCache);
            if(!goOn) return;
        }
    }
    
    /** List the values of the column in this table. */
    public <T> ListExt<T> listValues(int column, Class<T> valueType) {
        ListExt<T> list = new LinkedListExt<T>();
        int rc = getRowCount();
        for (int i = 0; i < rc; i++) {
            Object value = getValueAt(i, column);
            if(value != null && valueType.isAssignableFrom(value.getClass())) {
                list.add(valueType.cast(value));
            }
        }
        return list;
    }
    
    /** Checks if this table contains the given value (equals). If value is null, returns false. */
    public boolean containsValue(Object value) {
        if(value == null) return false;
        int rc = getRowCount();
        int cc = getColumnCount();
        for (int r = 0; r < rc; r++) {
            for (int c = 0; c < cc; c++) {
                Object valueAt = getValueAt(r, c);
                if(value.equals(valueAt)) return true;
            }
        }
        return false;
    }
    
    public JTableExt withColumnClass(int column, Class<?> c) {
        colClasses.put(column, c);
        return this;
    }
    
    public JPopupMenuExt getDefaultTextEditorPopup() {
        return defaultTextEditorPopup;
    }

    @Override
    public Class<?> getColumnClass(int column) {
        final Class<?> c = colClasses == null ? Object.class : colClasses.get(column);
        return c != null ? c : Object.class;
    }
    
    /** Executes a function on row-column indices when the user double clicks a table cell. Pass the row-col tuple to
     the callback. Active just if the event targets a cell*/
    public JTableExt onDoubleClick(final Function1<Tuple2<Integer, Integer>, ?> callback) {
        addMouseListener(new MouseAdapterExt() {

            @Override
            protected void mouseClickedExt(MouseEventExt e) {
                if(e.isDoubleClick() && e.isLeft()) {
                    int row = rowAtPoint(e.getPoint());
                    int col = columnAtPoint(e.getPoint());
                    if(row >= 0 && col >= 0) {
                        callback.apply(Tuple2.newInstance(row, col));
                    }
                }
            }
        });
        return this;
    }
    
    public void groupByColumnValue(int colIndex, final String value) {
        final Collator coll = Collator.getInstance();
        sort(new Function2<Integer, Integer, Integer>() {

            public Integer apply(Integer a, Integer b) {
                String var = String.valueOf(getValueAt(a, 0));
                return coll.compare(var, value) == 0 ? -1 : 1;
            }
        });
    }
    
    public void sortByColumnText(int colIndex) {
        final Collator coll = Collator.getInstance();
        sort(new Function2<Integer, Integer, Integer>() {

            public Integer apply(Integer a, Integer b) {
                String va = String.valueOf(getValueAt(a, 0));
                String vb = String.valueOf(getValueAt(b, 0));
                return coll.compare(va, vb);
            }
        });
    }
    
    /**
     * Sorts this table.
     * @param fun a function that takes two row indices and returns -1 if the first row should come first, 0 if
     * the rows are equal, 1 if the second row is greater that the second one
     */
    public void sort(final Function2<Integer, Integer, Integer> fun) {
        final int rc = getRowCount();
        final int cc = getColumnCount();
        if(rc == 0) return;
        
        final ArrayList<Integer> sortedRowIndices = new ArrayList<Integer>(rc);
        for(int i = 0; i < rc; i++) sortedRowIndices.add(i);
        Collections.sort(sortedRowIndices, new Comparator<Integer>() {

            public int compare(Integer o1, Integer o2) {
                return fun.apply(o1, o2);
            }
        });
        
        final DefaultTableModel model = getModel();
        final Vector<?> dataVector = model.getDataVector();
        
        final ArrayList<ArrayList<Object>> sortedData = new ArrayList<ArrayList<Object>>(rc);
        for (Integer rowIndex : sortedRowIndices) {
            final Vector<?> row = Vector.class.cast(dataVector.get(rowIndex));
            sortedData.add(new ArrayList<Object>(row));
        }
        for (int i = 0; i < rc; i++) {
            final ArrayList<Object> rowData = sortedData.get(i);
            for (int j = 0; j < cc; j++) {
                final Object cell = rowData.get(j);
                model.setValueAt(cell, i, j);
            }
        }
    }
    
    public void setRow(int rowIndex, Object... values) {
        for(int i = 0; i < getColumnCount() && i < values.length; i++) {
            setValueAt(values[i], rowIndex, i);
        }
    }
    
    public <T> JTableExt withSimpleRenderer(final Class<T> valueType, final Function3<T, JLabel, Point, ?> renderer) {
        return withRenderer(valueType, new DefaultTableCellRenderer() {

            /* default serial version code */
            private static final long serialVersionUID = 0;

            @Override
            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
                JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                renderer.apply(valueType.cast(value), label, new Point(row, column));
                return label;
            }
        });
    }

    public <T> JTableExt withSimpleRenderer(final Class<T> valueType, final Function2<T, JLabel, ?> renderer) {
        return withRenderer(valueType, new DefaultTableCellRenderer() {

            /* default serial version code */
            private static final long serialVersionUID = 0;

            @Override
            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
                JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                renderer.apply(valueType.cast(value), label);
                return label;
            }
        });
    }

    public JTableExt withRenderer(Class<?> valueType, TableCellRenderer renderer) {
        super.setDefaultRenderer(valueType, renderer);
        return this;
    }

    public JTableExt withEditor(Class<?> valueType, TableCellEditor editor) {
        super.setDefaultEditor(valueType, editor);
        return this;
    }

    public JTableExt hideTableHeader() {
        getTableHeader().setVisible(false);
        return this;
    }

    public JScrollPane getScroller() {
        if(scroller == null) scroller = new JScrollPane(this);
        scroller.setName(getName());
        return scroller;
    }

    @Override
    public void setModel(TableModel model) {
        if(!(model instanceof DefaultTableModel)) {
            throw new UnsupportedOperationException("jtable ext works with default table models only");
        } else {
            super.setModel(model);
        }
    }

    @Override
    public void setValueAt(Object aValue, int row, int column) {
        if(row < getRowCount() && column < getColumnCount()) {
            super.setValueAt(aValue, row, column);
        } else {
            Logger.getLogger(JTableExt.class.getName()).log(Level.INFO, "set value at out of bounds");
        }
    }

    /**
     * Called during construction
     * @return the model of this table
     */
    @Override
    public final DefaultTableModel getModel() {
        return (DefaultTableModel) super.getModel();
    }
    
    public JTableExt append(Object...values) {
        appendRow(true, values);
        return this;
    }

    public void appendRow(boolean notifyListeners, Object... values) {
        if(!notifyListeners) detachTableModelListeners(getModel());
        getModel().insertRow(getRowCount(), values);
        if(!notifyListeners) attachTableModelListeners(getModel());
    }

    public JTableExt onModelChangeEvent(final Runnable task) {
        getModel().addTableModelListener(new TableModelListener() {

            public void tableChanged(TableModelEvent e) {
                task.run();
            }
        });
        return this;
    }
    
    public JTableExt onSelectionChange(final Runnable task) {
        getSelectionModel().addListSelectionListener(new ListSelectionListener() {

            public void valueChanged(ListSelectionEvent e) {
                task.run();
            }
        });
        return this;
    }

    public JTableExt setSingleRowSelection() {
        getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        return this;
    }

    @Override
    public TableCellRenderer getCellRenderer(int row, int column) {
        TableCellRenderer renderer = null;
        Object value = getValueAt(row, column);
        if(value != null) {
            renderer = getDefaultRenderer(value.getClass());
        }
        if(renderer == null) renderer = super.getCellRenderer(row, column);
        return renderer;
    }

    @Override
    public TableCellEditor getCellEditor(int row, int column) {
        if(columnEditors.containsKey(column)) return columnEditors.get(column);
        
        TableCellEditor editor = null;
        Object value = getValueAt(row, column);
        if(value != null) {
            editor = getDefaultEditor(value.getClass());
        }
        if(editor == null) editor = super.getCellEditor(row, column);
        return editor;
    }
    
    /** Sets the value of the first column as row labels */
    public JTableExt withRowNames(String... values) {
        if(getColumnCount() > 0) for (int i = 0; i < values.length && i < getRowCount(); i++) {
            String label = values[i];
            setValueAt(label, i, 0);
        }
        hasRowNames = true;
        return this;
    }

    @Override
    public boolean isCellEditable(int row, int column) {
        if(hasRowNames && column == 0) return false;
        
        if(notEditable) {
            return false;
        }
        if(editableColumns != null) {
            return column < editableColumns.length && editableColumns[column];
        }
        return super.isCellEditable(row, column);
    }

    public JTableExt setEditableColumns(boolean... states) {
        editableColumns = states;
        return this;
    }
    
    public JTableExt setNotEditable() {
        notEditable = true;
        return this;
    }

    public JTableExt withColumnNames(Object... titles) {
        int colCount = getColumnCount();
        int colToAdd = titles.length - colCount;
        if(colToAdd > 0) {
            getModel().setColumnCount(colCount + colToAdd);
        }
        for (int i = 0; i < titles.length; i++) {
            Object title = titles[i];
            getColumnModel().getColumn(i).setHeaderValue(title);
        }
        return this;
    }

    public JTableExt withColumnSizes(int... sizes) {
        for (int i = 0; i < sizes.length; i++) {
            TableColumn column = getColumnModel().getColumn(i);
            column.setWidth(sizes[i]);
            column.setPreferredWidth(sizes[i]);
            column.setMaxWidth(sizes[i]);
        }
        return this;
    }

    private final ListExt<TableModelListener> DETACHED_LISTENERS = new LinkedListExt<TableModelListener>();

    public JTableExt clear(boolean fireEvent) {
        final int rowsToRemove = getRowCount();
        final DefaultTableModel model = getModel();
        if(!fireEvent) {
            detachTableModelListeners(model);
        }
        for (int i= 0; i < rowsToRemove; i++) {
            model.removeRow(0);
        }
        if(!fireEvent) {
            attachTableModelListeners(model);
        }
        return this;
    }

    private void attachTableModelListeners(final DefaultTableModel model) {
        for (int i = 0; i < DETACHED_LISTENERS.size(); i++) {
            model.addTableModelListener(DETACHED_LISTENERS.get(i));
        }
    }

    private void detachTableModelListeners(final DefaultTableModel model) {
        DETACHED_LISTENERS.setAll(model.getTableModelListeners());
        for (int i = 0; i < DETACHED_LISTENERS.size(); i++) {
            model.removeTableModelListener(DETACHED_LISTENERS.get(i));
        }
    }

    /**
     * Iterates over the rows of this table
     * @return the row iterator
     */
    public Iterator<ListExt<?>> iterator() {
        return new Iterator<ListExt<?>>() {
            final DefaultTableModel model = getModel();
            final int colCount = model.getColumnCount();
            int row = 0;

            public boolean hasNext() {
                return row < getRowCount();
            }

            public ListExt<?> next() {
                ListExt<Object> list = new ArrayListExt<Object>(colCount);
                for (int i= 0; i < colCount; i++) {
                    list.add(model.getValueAt(row, i));
                }
                row++;
                return list;
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    /**
     * Discards the editor and removed the given row
     * @param selectedRow the row to remove
     */
    public void removeRow(int selectedRow) {
        if(isEditing()) removeEditor();
        getModel().removeRow(selectedRow);
        rowTags.remove(selectedRow);
    }

    /**
     * Sets the row margin and returns this
     * @param margin the new row margin
     * @return this
     */
    public JTableExt withRowMargin(int margin) {
        setRowMargin(margin);
        return this;
    }

    /**
     * Sets the row height and returns this
     * @param height the new row height
     * @return this
     */
    public JTableExt withRowHeight(int height) {
        setRowHeight(height);
        return this;
    }

    public JTableExt withGridPainted(boolean on) {
        setShowGrid(on);
        return this;
    }

    public void addRow(Object... elements) {
        if(elements != null) {
            getModel().addRow(elements);
        }
    }

    /**
     * Removes all rows from the table
     */
    public void clear() {
        DefaultTableModel model = getModel();
        Object[] columnNames = new Object[model.getColumnCount()];
        for (int i = 0; i < columnNames.length; i++) {
            columnNames[i] = model.getColumnName(i);
        }
        Object[][] values = new Object[0][columnNames.length];
        model.setDataVector(values, columnNames);
        rowTags.clear();
    }

    public void clearValues() {
        for (int i= 0; i < getRowCount(); i++) {
            for (int j= 0; j < getColumnCount(); j++) {
                setValueAt(null, i, j);
            }
        }
        rowTags.clear();
    }

    public void commitEdit() {
        TableCellEditor editor = getCellEditor();
        if(editor != null) {
            editor.stopCellEditing();
        }
    }

    public void clearRows() {
        DefaultTableModel model = getModel();
        rowTags.clear();
        model.setRowCount(0);
    }
    
    /** Returns an array of values associated with the row at the given index. */
    public Object[] getRowTags(int rowIndex, Object[] initialValue) {
        Object[] tags = rowTags.get(rowIndex);
        if(tags == null) rowTags.put(rowIndex, tags = initialValue);
        return tags;
    }

    public void removeRowWithValueAtColumn(Object value, int column) {
        if(value == null) return;
        if(column >= getColumnCount()) return;
        
        for(int i = 0; i < getRowCount(); i++) {
            Object cellValue = getValueAt(i, column);
            if(cellValue != null && cellValue.equals(value)) {
                removeRow(i);
                break;
            }
        }
    }

    public JTableExt setNoSelectionAllowed() {
        this.setColumnSelectionAllowed(false);
        this.setRowSelectionAllowed(false);
        return this;
    }

    public JTableExt withName(String string) {
        setName(string);
        return this;
    }

    public JTableExt singleRowSelection() {
        setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        return this;
    }
    
    public XPrint createReportFromSelectedRows(String reportTableName) {
        XPrint print = new XPrint();
        XTable table = print.appendTable(reportTableName);
        int[] rows = getSelectedRows();
        int[] columns = getSelectedColumns();
        for (int r = 0; r < rows.length; r++) {
            XTableRow tableRow = table.appendRow();
            int row = rows[r];
            for (int c = 0; c < columns.length; c++) {
                int col = columns[c];
                XTableCell cell = tableRow.addCell();
                cell.setText(String.valueOf(getValueAt(row, col)));
            }
        }
        return print;
    }

    public JTableExt withDefaultTextEditorPopupMenu() {
        TableCellEditor stringEditor = this.getDefaultEditor(String.class);
        if(stringEditor != null) try {
            Component editor = stringEditor.getTableCellEditorComponent(this, "", true, 0, 0);
            if(editor instanceof JTextComponent) {
                defaultTextEditorPopup = JPopupMenuExt.newDefaultTextPopup((JTextComponent) editor);
            }
        } catch(Exception ex) {
            Log.message("Cannot get the default text editor to set the copy/cut/past/clear menu.");
        }
        return this;
    }

    public void insertRowAtPoint(Point p2, Object[] rowData) {
        int rowIndex = rowAtPoint(p2);
        if(rowIndex >= 0) {
            this.getModel().insertRow(rowIndex, rowData);
        }
    }
}
